﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace RFEngine
{
    public class Camera
    {
        #region Fields

        private Viewport _ViewPort;
        private GraphicsDevice _GraphicsDevice;

        private bool _ViewChanged;
        private bool _ProjectionChanged;

        private float _AspectRatio;
        private float _Width = 1.0f;
        private float _Height = 1.0f;
        private float _FieldOfView = 72.0f;
        private float _NearPlane = 0.001f;
        private float _FarPlane = 1000.0f;

        private float _ViewportHeight = 1.0f;
        private float _ViewportWidth = 1.0f;
        private float _ViewportX = -1.0f;
        private float _ViewportY = 1.0f;

        private Vector3 _Position;
        private Vector3 _Rotation = Vector3.Zero;
        private Vector3 _RotationRate = Vector3.Zero;
        private Vector3 _Direction = Vector3.Forward;
        private Vector3 _LookAt = Vector3.Zero;
        private Vector3 _CameraFoward = Vector3.Up;
        private Vector3 _CameraUp = Vector3.Up;

        private Color _BackgroundColor = Color.TransparentBlack;

        private CameraType _CameraType = CameraType.Perspective;
        private CameraLayer _DefaultLayer;
        private List<CameraLayer> _CameraLayerCollection = new List<CameraLayer>();

        private Matrix _ViewMatrix = Matrix.Identity;
        private Matrix _ProjectionMatrix = Matrix.Identity;
        private Matrix _RotationMatrix = Matrix.Identity;
        private Matrix _TranslationMatrix = Matrix.Identity;

        private BoundingFrustum _BoundingFrustum;

        #endregion

        #region Properties

        #region Projection Properties

        /// <summary>
        /// Gets or Sets the aspect ratio of the projection matrix.
        /// </summary>
        public float AspectRatio
        {
            get { return _AspectRatio; }
            set
            {
                _AspectRatio = value;
                _ProjectionChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Field of view in degrees for this object.
        /// </summary>
        public float FieldOfView
        {
            get { return _FieldOfView; }
            set
            {
                _FieldOfView = value;

                if (_FieldOfView <= 0)
                    _FieldOfView = float.Epsilon;
                else if (_FieldOfView >= 180.0f)
                    _FieldOfView -= float.Epsilon;

                _ProjectionChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the minimum view distance of this camera.
        /// </summary>
        public float NearPlane
        {
            get { return _NearPlane; }
            set
            {
                _NearPlane = value;

                if (_NearPlane <= 0)
                    _NearPlane = float.Epsilon;

                _ProjectionChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Maximum view distance of this camera.
        /// </summary>
        public float FarPlane
        {
            get { return _FarPlane; }
            set
            {
                _FarPlane = value;
                if (_FarPlane <= 0)
                    _FarPlane = float.Epsilon;

                _ProjectionChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Width of the camera view, when in Orthographic projection mode.
        /// </summary>
        public float Width
        {
            get { return _Width; }
            set
            {
                _Width = value;

                if (_CameraType == CameraType.Orthographic)
                    _ProjectionChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Height of the camera view, when in Orthographic projection mode.
        /// </summary>
        public float Height
        {
            get { return _Height; }
            set
            {
                _Height = value;

                if (_CameraType == CameraType.Orthographic)
                    _ProjectionChanged = true;
            }
        }

        #endregion

        #region Position Properties

        /// <summary>
        /// Gets or Sets the position of this object in world space along the x-axis
        /// </summary>
        public float X
        {
            get { return _Position.X; }
            set 
            { 
                _Position.X = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the position of this object in world space along the y-axis
        /// </summary>
        public float Y
        {
            get { return _Position.Y; }
            set 
            { 
                _Position.Y = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the position of this object in world space along the z-axis
        /// </summary>
        public float Z
        {
            get { return _Position.Z; }
            set
            {
                _Position.Z = value;
                _ViewChanged = true;
            }
        }

        #endregion

        #region Rotation Properties

        /// <summary>
        /// Gets or Sets the rotation of this object along the x-axis
        /// </summary>
        public float XRotation
        {
            get { return _Rotation.X; }
            set 
            { 
                _Rotation.X = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Rate of rotation change for the x rotational value, in rotations per second.
        /// </summary>
        public float XRotationRate
        {
            get { return _RotationRate.X; }
            set
            {
                _RotationRate.X = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the rotation of this object along the y-axis
        /// </summary>
        public float YRotation
        {
            get { return _Rotation.Y; }
            set 
            { 
                _Rotation.Y = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the Rate of rotation change for the y rotational value, in rotations per second.
        /// </summary>
        public float YRotationRate
        {
            get { return _RotationRate.Y; }
            set
            {
                _RotationRate.Y = value;
                _ViewChanged = true;
            }
        }

        /// <summary>
        /// Gets or Sets the rotation of this object along the z-axis
        /// </summary>
        public float ZRotation
        {
            get { return _Rotation.Z; }
            set { _Rotation.Z = value; }
        }

        /// <summary>
        /// Gets or Sets the Rate of rotation change for the z rotational value, in rotations per second.
        /// </summary>
        public float ZRotationRate
        {
            get { return _RotationRate.Z; }
            set
            {
                _RotationRate.Z = value;
                _ViewChanged = true;
            }
        }

        #endregion

        /// <summary>
        /// Gets the default camera layer for this camera.
        /// </summary>
        public CameraLayer DefaultLayer
        {
            get { return _DefaultLayer; }
        }

        /// <summary>
        /// Gets or Sets the type of camera this camera represents
        /// </summary>
        public CameraType CameraType
        {
            get { return _CameraType; }
            set
            {
                if (_CameraType != value)
                {
                    _CameraType = value;
                    _ProjectionChanged = true;
                }
            }
        }

        /// <summary>
        /// Gets the position of this object in world space.
        /// </summary>
        public Vector3 Position
        {
            get { return _Position; }
        }

        /// <summary>
        /// Gets the rotation of this object.
        /// </summary>
        public Vector3 Rotation
        {
            get { return _Rotation; }
        }

        /// <summary>
        /// Gets the view matrix of this camera.
        /// </summary>
        public Matrix View
        {
            get { return _ViewMatrix; }
        }

        /// <summary>
        /// Gets the Projection matrix of this camera.
        /// </summary>
        public Matrix Projection
        {
            get { return _ProjectionMatrix; }
        }

        /// <summary>
        /// Gets the Bounding Frustum of this camera.
        /// </summary>
        public BoundingFrustum BoundingFrustum
        {
            get { return _BoundingFrustum; }
        }

        #endregion

        #region Methods

        #region Constructor(s)

        /// <summary>
        /// Constructs a new instance of this object.
        /// </summary>
        /// <param name="graphicsDevice">The GraphicsDevice to use when creating a new instance.</param>
        public Camera(GraphicsDevice graphicsDevice)
        {
            _ViewPort = graphicsDevice.Viewport;
            _GraphicsDevice = graphicsDevice;

            _Position = new Vector3(0, 0, 50);
            _AspectRatio = graphicsDevice.Viewport.AspectRatio;

            SetView();
            SetProjection();

            _BoundingFrustum = new BoundingFrustum(_ViewMatrix * _ProjectionMatrix);
            _DefaultLayer = AddCameraLayer();
        }

        #endregion

        /// <summary>
        /// Method for adding a single CameraLayer to this camera.
        /// </summary>
        /// <returns></returns>
        public CameraLayer AddCameraLayer()
        {
            _CameraLayerCollection.Add(new CameraLayer(this));

            return _CameraLayerCollection.Last();
        }

        /// <summary>
        /// Method for adding an already existing camera layer to this camera.
        /// </summary>
        /// <param name="item"></param>
        public void AddCameraLayer(CameraLayer item)
        {
            item.AddCamera(this);
            _CameraLayerCollection.Add(item);
        }

        /// <summary>
        /// Method for removing a single CameraLayer from this camera.
        /// </summary>
        /// <param name="item"></param>
        public void RemoveLayer(CameraLayer item)
        {
            if (item != _DefaultLayer)
            {
                if (_CameraLayerCollection.Remove(item))
                    item.RemoveCamera(this);
            }
        }

        /// <summary>
        /// Method for Setting the background clear color for when this is called to draw.
        /// </summary>
        /// <param name="color"></param>
        public void SetBackgroundColor(Color color)
        {
            _BackgroundColor = color;
        }

        /// <summary>
        /// Method for preparing the graphics device before drawing.
        /// </summary>
        public void PrepareGraphicsDevice()
        {
            _ViewPort.Height = (int)(_ViewportHeight * _GraphicsDevice.PresentationParameters.BackBufferHeight);
            _ViewPort.Width = (int)(_ViewportWidth * _GraphicsDevice.PresentationParameters.BackBufferWidth);
            _ViewPort.X = (int)(((_ViewportX + 1) / 2.0f) * (float)_GraphicsDevice.PresentationParameters.BackBufferWidth);
            _ViewPort.Y = (int)(((-_ViewportY + 1) / 2.0f) * (float)_GraphicsDevice.PresentationParameters.BackBufferHeight);

            _GraphicsDevice.Viewport = _ViewPort;
            _GraphicsDevice.Clear(_BackgroundColor);
        }

        #region Private Methods

        /// <summary>
        /// Method for forcing a view matrix update.
        /// </summary>
        private void SetView()
        {
            Matrix.CreateTranslation(ref _Position, out _TranslationMatrix);
            Matrix.CreateFromYawPitchRoll(MathHelper.ToRadians(_Rotation.X), MathHelper.ToRadians(_Rotation.Y), MathHelper.ToRadians(_Rotation.Z), out _RotationMatrix);
            Vector3.Transform(ref _Position, ref _RotationMatrix, out _Direction);

            _Direction.Normalize();
            _LookAt = _Position - _Direction;

            Vector3.Transform(ref _CameraFoward, ref _RotationMatrix, out _CameraUp);
            Matrix.CreateLookAt(ref _Position, ref _LookAt, ref _CameraUp, out _ViewMatrix);
        }

        /// <summary>
        /// Method for forcing a projcetion matrix update.
        /// </summary>
        private void SetProjection()
        {
            switch (_CameraType)
            {
                case CameraType.Orthographic:

                    Matrix.CreateOrthographic(_Width, _Height, _NearPlane, _FarPlane, out _ProjectionMatrix);
                    break;

                case CameraType.Perspective:

                    Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(_FieldOfView), _AspectRatio, _NearPlane, _FarPlane, out _ProjectionMatrix);
                    break;
            }            
        }

        /// <summary>
        /// Method for forcing an update on this cameras BoundingFrustum.
        /// </summary>
        private void UpdateBoundingFrustum()
        {
            _BoundingFrustum.Matrix = _ViewMatrix * _ProjectionMatrix;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Method for Updating the state of this object.
        /// </summary>
        internal void Update(float updateTime)
        {
            if (_ViewChanged)
                SetView();

            if (_ProjectionChanged)
                SetProjection();

            if (_ViewChanged || _ProjectionChanged)
                UpdateBoundingFrustum();

            _ViewChanged = false;
            _ProjectionChanged = false;
        }

        #endregion

        #endregion
    }
}
